<?php
/**
 * This Source Code Form is subject to the terms
 * of the Mozilla Public License, v. 2.0.
 * If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/.
 */

/**
 * Serializes and unserializes xmls <==> objects
 * @author Albert Ovide <albert at ovide dot net>
 * @since 05-09-2012
 * @version 1.0
 */
abstract class XmlSerializable {

	const DEFAULT_EMPTY = '';
	const DEFAULT_ZERO  = '0';
	const DEFAULT_DATE  = '1900-01-01';

	/**
	 * Defines required vars (key) and its default values (value) for the xml.
	 * This property must be overridden by child classes when needed.
	 * @example
	 * 	protected $__required_vars=array(
	 * 		'id'   => self::DEFAULT_ZERO,
	 * 		'name' => self::DEFAULT_EMPTY
	 * 	);
	 * @var array
	 */
	protected $__required_vars=array();
	/**
	 * @var string Text Nodevalue
	 */
	protected $__nodeValue;
	/**
	 * Callbacks' list to check when a property is modified.
	 * @see addCallback
	 * @var string[]
	 */
	protected $__callbacks=array();

	/**
	 * @param string $nodeValue Sets the content for the nodeValue
	 */
	public final function __construct($nodeValue=null){
		if($nodeValue!=null)$this->setNodeValue($nodeValue);
	}
	/**
	 * Returns the nodevalue if exists, else returns the xml serialization
	 * @return string
	 */
	public function __toString(){
		return (isset($this->__nodeValue))?
			$this->__nodeValue:$this->xmlSerialize(true);
	}
	/**
	 * @param bool $format Formats xml output
	 * @return string The serialized xml
	 */
	public function xmlSerialize($format=false){
		$document=$this->toDOMDocument();
		$document->formatOutput=$format;
		return $document->saveXML();
	}
	/**
	 * Constructs a instance of the object from a representative xml
	 * @param string $xml
	 */
	public function xmlUnserialize($xml){
		$document=new DOMDocument();
		$document->loadXML($xml);
		$special=array_keys(get_class_vars(__CLASS__));
		$class=get_class($this);
		$reflection=new ReflectionClass($class);
		$vars=array_keys($reflection->getDefaultProperties());
		/* @var $root DOMElement */
		$root=$document->documentElement;
		$nodes=array();
		/* @var $childNode DOMNode */
		foreach($root->childNodes as $childNode){
			$nodeName=$childNode->nodeName;
			//for some reason, gets nodes with '#text' name we must clean
			if($nodeName[0]!='#')$nodes[]=$nodeName;
		}
		if($root->tagName==$class){
			foreach($vars as $var){
				if(in_array($var,$special))continue;
				if(!is_array($this->$var)){
					if($root->hasAttribute($var)){
						$this->$var=$root->getAttribute($var);
					}elseif(in_array($var, $nodes)){
						unset($this->__nodeValue);
						$node=$root->getElementsByTagName($var)->item(0);
						$child_xml=$root->ownerDocument->saveXML($node);
						$this->$var=new $var($node->nodeValue);
						$this->$var->xmlUnserialize($child_xml);
					}
				}else{
					if(in_array($var, $nodes)){
						$nodes=$root->getElementsByTagName($var);
						foreach($nodes as $node){
							unset($this->__nodeValue);
							$child_xml=$root->ownerDocument->saveXML($node);
							$tmp=new $var($node->nodeValue);
							$tmp->xmlUnserialize($child_xml);
							array_push($this->$var,$tmp);
						}
					}
				}
			}
		}
	}
	/**
	 * @return string Gets the node value
	 */
	public function getNodeValue(){
		return(isset($this->__nodeValue) && $this->__nodeValue!='')?
			$this->__nodeValue:
			'';
	}
	/**
	 * @param string $value Sets the node value
	 */
	public function setNodeValue($value){
		$this->__nodeValue=$value;
	}
	/**
	 * Adds a callback which will be called for every property modification.
	 * The called method allways will be like "on$Class$PropertyChanged"
	 * @example
	 *	$parent=new Parent();
	 *	$parent->xmlUnserialize($xml);
	 *	//@var $child Child
	 *	$child=$parent->$childs[0];
	 *	$object->addCallback('val',$parent);
	 *	$object->Val=4; //$parent->onChildValChanged() whill be called.
	 *
	 * @param string $attr The property which will call back a method
	 * @param XmlSerializable $object Object which contains the method
	 */
	public final function addCallback($attr,$object){
		$this->__callbacks[$attr][]=$object;
	}
	/**
	 * Read access for all the xml attributes.
	 * @param string $name Property name
	 * @return mixed
	 * @throws LogicException If the property doesn't exist
	 */
	public final function __get($name){
		$class=get_class($this);
		if(substr($name,0,1)=='_')
				throw new LogicException("'$name' isn't a valid property name");
		$reflection=new ReflectionClass($class);
		$attr_arr=$reflection->getDefaultProperties();
		$attrib=(array_key_exists($name,$attr_arr))?$name:lcfirst($name);
		if(array_key_exists($attrib,$attr_arr))return $this->$attrib;
		elseif(array_key_exists($name,$attr_arr))return $this->$name;
		else throw new LogicException("'$name' isn't a $class attribute");
	}
	/**
	 * Write access for all the xmls attributes
	 *
	 * If there's some method with a name like set$Attribute,
	 * will call the setter instead of set the property.
	 *
	 * @param string $name
	 * @param mixed $value
	 * @throws LogicException
	 */
	public final function __set($name, $value) {
		$class=get_class($this);
		if(substr($name,0,1)=='_')
				throw new LogicException("'$name' isn't a valid property name");
		$reflection=new ReflectionClass($class);
		$attr_arr=$reflection->getDefaultProperties();
		$attrib=(array_key_exists($name,$attr_arr))?$name:lcfirst($name);
		if(array_key_exists($attrib,$attr_arr)){
			//checks for a setter
			$methods=get_class_methods($class);
			$setter="set$name";
			if(in_array($setter, $methods)) $this->$setter($value);
			else $this->$attrib=$value;
			//checks for callbacks
			$uc_class=ucfirst($class);
			if(isset($this->__callbacks[$attrib])){
				$method="on$uc_class$name".'Changed';
				foreach($this->__callbacks[$attrib] as $object)
					$object->$method();
			}
		}
		else throw new LogicException("'$name' isn't a $class attribute");
	}
	/**
	 * Used to implement the add$Node() method
	 * @param string $add Name of the method.
	 * The name must start with 'add' and end with the property name.
	 * The property must be an array.
	 * @param XmlSerializable $nodes
	 */
	public final function __call($add, $nodes) {
		foreach($nodes as $node){
			if(!is_a($node, __CLASS__))
				throw new LogicException('One argument is not '.__CLASS__);
		}
		$PREFIX='add';
		if(!substr($add,0,strlen($PREFIX))==$PREFIX)
			throw new LogicException("'$add' method not found");
		else{
			//For $add='addNode' => $attrib='Node'
			$attrib=substr($add,strlen($PREFIX));
			$this->addNodes($attrib, $nodes);
		}
	}
	/**
	 * @param DOMElement
	 * @return DOMNode
	 */
	protected final function toDOMDocument($parent=null){
		if($parent==null) $parent=new DOMDocument('1.0','UTF-8');
		/* @var $special array Special attributes to be ignored */
		$special=array_keys(get_class_vars(__CLASS__));
		$class=get_class($this);
		$function=__FUNCTION__;
		$reflection=new ReflectionClass($class);
		$vars=array_keys($reflection->getDefaultProperties());
		/* @var $document DOMDocument */
		$document=($parent instanceof DOMDocument)?
			$parent:$parent->ownerDocument;

		$element=($this->getNodeValue()!='')?
			$document->createElement($class,$this->getNodeValue()):
			$document->createElement($class);
		foreach($vars  as $attrib){
			if(in_array($attrib,$special))continue;
			if(is_a($this->$attrib,__CLASS__)){
				$this->$attrib->$function($element);
			}elseif(is_array($this->$attrib)){
				foreach($this->$attrib as $node){
					if(is_a($node,__CLASS__)) $node->$function($element);
				}
			}else{
				$value=null;
				if($this->$attrib!=null) $value=$this->$attrib;
				else $value=$this->isRequiredAttribute($attrib);
				if($value!==null && !is_object($value)){
					$search=array('"','<','>','&');
					$replace=array('\"','&amp;','&lt;','&gt;');
					$value=str_replace($search, $replace, $value);
					$element->setAttribute($attrib, $value);
				}
			}
		}
		$parent->appendChild($element);
		return $parent;
	}
	/**
	 * Adds objects ($nodes) to $name
	 * @param string $attrib Property name
	 * @param XmlSerializable[] $nodes
	 */
	private function addNodes($name, $nodes){
		$class=get_class($this);
		$reflection=new ReflectionClass($class);
		$attr_arr=$reflection->getDefaultProperties();
		$attrib=(array_key_exists($name,$attr_arr))?$name:lcfirst($name);
		if(array_key_exists($attrib,$attr_arr) && is_array($this->$attrib)){
			foreach($nodes as $node) array_push($this->$attrib,$node);
		}
	}
	/**
	 * @param string $attrib Property name to check
	 * @return string or null if not required or default value if required
	 */
	private function isRequiredAttribute($attrib){
		return isset($this->__required_vars[$attrib])?
			$this->__required_vars[$attrib]:null;
	}
}

//For PHP<5.3
if (false === function_exists('lcfirst')){
	function lcfirst($str)
		{return (string)(strtolower(substr($str,0,1)).substr($str,1));}
}
if (false === function_exists('ucfirst')){
    function ucfirst($str)
    	{return (string)(strtoupper(substr($str,0,1)).substr($str,1));}
}

